//! Partial tick aware clock.
//!
//! This clock is a bit weird because we don't want to always be measuring world
//! time relative to 0.  This is because when we load a world from disk we are
//! resuming it at some particular tick, not at 0.  Similarly, if we aren't
//! running the world fast enough and are lagging behind temporarily, we will
//! have to slide the clock a bit.  It feels most elegant to represent this by
//! actually keeping track of the tick we resumed at and when is the real time
//! of that tick in the actively running simulation.

use std::time;

pub struct Clock {
    /// Base tick real time.
    base_ts: time::Instant,

    /// The tick we do measurements from.  This might update if we have to skip ticks.
    base_tick: i64,

    /// Tick duration, in micros.
    tick_dur_us: u64,
}

impl Clock {
    /// Constructs a new instance with "now" as a base tick time at 0, with some
    /// tick duration.
    ///
    /// # Panics
    ///
    /// If passing 0 for tick duration.
    pub fn new_now_at_0(tick_dur: time::Duration) -> Self {
        Self::new(get_now(), 0, tick_dur.as_micros() as u64)
    }

    /// Constructs a new instance.
    ///
    /// # Panics
    ///
    /// If passing 0 for tick duration.
    pub fn new(base_ts: time::Instant, base_tick: i64, tick_dur_us: u64) -> Self {
        if tick_dur_us == 0 {
            panic!("clock: can't have zero tick duration");
        }
        Self {
            base_ts,
            base_tick,
            tick_dur_us,
        }
    }

    /// Returns the base time.
    pub fn base_ts(&self) -> time::Instant {
        self.base_ts
    }

    /// Returns the base tick.
    pub fn base_tick(&self) -> i64 {
        self.base_tick
    }

    /// Returns the tick duration in micros.
    pub fn tick_dur_us(&self) -> u64 {
        self.tick_dur_us
    }

    /// Returns the tick duration as a [``time::Duration``].
    pub fn tick_dur(&self) -> time::Duration {
        time::Duration::from_micros(self.tick_dur_us())
    }

    /// Resets the base tick and and reference time.
    pub fn reset_base(&mut self, new_base_tick: i64, new_base_ts: time::Instant) {
        self.base_tick = new_base_tick;
        self.base_ts = new_base_ts;
    }

    /// Resets the base tick using the current time as reference.
    pub fn reset_base_now(&mut self, new_base_tick: i64) {
        self.reset_base(new_base_tick, get_now())
    }

    /// Gets the instant offset from the base by some number of micros.
    fn get_base_ts_off(&self, micros: i64) -> time::Instant {
        // Have to do some weird conditionals because we can't have negative durations.
        if micros > 0 {
            self.base_ts + time::Duration::from_micros(micros as u64)
        } else {
            self.base_ts - time::Duration::from_micros(-micros as u64)
        }
    }

    /// Gets the timestamp where the tick should start.
    pub fn get_tick_start(&self, tick: i64) -> time::Instant {
        let d = (tick - self.base_tick) as i64;
        self.get_base_ts_off(d * self.tick_dur_us() as i64)
    }

    /// Gets the micros relative to the reference time.  Positive means after, negative means before.
    fn get_relative_micros(&self, ts: time::Instant) -> i64 {
        match ts.checked_duration_since(self.base_ts) {
            Some(dur) => dur.as_micros() as i64,
            // This shouldn't panic if we get here.
            None => -(self.base_ts.duration_since(ts).as_micros() as i64),
        }
    }

    /// Gets the tick as of some micros offset from the base time.
    ///
    /// This accounts for the base tick being nonzero.
    fn get_tick_at_rel_micros(&self, off: i64) -> i64 {
        i64::div_floor(off, self.tick_dur_us() as i64) + self.base_tick()
    }

    /// Gets the expected tick for a given instant.  This is the most recently
    /// started tick, which may have be expected to start at this instant.
    pub fn get_expected_tick_at(&self, ts: time::Instant) -> i64 {
        // Calculate the micro time relative to the base timestamp.
        let ts_off = self.get_relative_micros(ts);
        self.get_tick_at_rel_micros(ts_off)
    }

    /// Computes the integer tick and the proportion through it some instant is.
    pub fn get_partial_tick_at(&self, ts: time::Instant) -> (i64, f32) {
        let exp_tick = self.get_expected_tick_at(ts);
        let exp_tick_start = self.get_tick_start(exp_tick);

        let micros_since = ts.duration_since(exp_tick_start).as_micros() as f32;
        let proportion = micros_since / (self.tick_dur_us() as f32);

        (exp_tick, proportion)
    }

    /// Returns an interpolation proportion, assuming we have some target
    /// current tick.
    ///
    /// If we've passed the target, returns 1.0, implying we just render at that
    /// tick state as-is.  If completely before the previous tick, returns 0.0,
    /// implying we should just render the previous tick state as-is.
    pub fn get_target_partial_tick(&self, target_tick: i64, check_ts: time::Instant) -> f32 {
        let tick_start_ts = self.get_tick_start(target_tick - 1);
        if check_ts < tick_start_ts {
            return 0.0;
        }

        let elapsed_us = check_ts.duration_since(tick_start_ts).as_micros() as u64;
        if elapsed_us >= self.tick_dur_us() {
            return 1.0;
        }

        (elapsed_us as f32) / (self.tick_dur_us() as f32)
    }
}

fn get_now() -> time::Instant {
    time::Instant::now()
}

#[cfg(test)]
mod tests {
    use std::time;

    use super::Clock;

    fn test_approx_eq(a: f32, b: f32, thresh: f32) -> bool {
        (a - b).abs() < thresh
    }

    #[test]
    fn test_tick_start() {
        let tick_dur_us = 50 * 1000;
        let tick_dur = time::Duration::from_micros(tick_dur_us);
        let base_ts = time::Instant::now();

        let clock = Clock::new(base_ts, 0, tick_dur_us);

        assert_eq!(clock.get_tick_start(0), base_ts, "test: check tick 0");

        assert_eq!(
            clock.get_tick_start(1),
            base_ts + tick_dur,
            "test: check tick 1"
        );

        assert_eq!(
            clock.get_tick_start(100),
            base_ts + tick_dur * 100,
            "test: check tick 100"
        );

        assert_eq!(
            clock.get_tick_start(-5),
            base_ts - tick_dur * 5,
            "test: check tick -5"
        );
    }

    #[test]
    fn test_get_expected() {
        let tick_dur_us = 50 * 1000;
        let tick_dur = time::Duration::from_micros(tick_dur_us);
        let tiny_dur = time::Duration::from_millis(1);
        let base_ts = time::Instant::now();

        let clock = Clock::new(base_ts, 0, tick_dur_us);

        assert_eq!(clock.get_expected_tick_at(base_ts), 0, "test: check tick 0");

        assert_eq!(
            clock.get_expected_tick_at(base_ts + tiny_dur),
            0,
            "test: check tick 0 + tiny"
        );

        assert_eq!(
            clock.get_expected_tick_at(base_ts + tick_dur),
            1,
            "test: check tick 1"
        );

        assert_eq!(
            clock.get_expected_tick_at(base_ts + tick_dur + tiny_dur),
            1,
            "test: check tick 1 + tiny"
        );

        assert_eq!(
            clock.get_expected_tick_at(base_ts - tick_dur * 5),
            -5,
            "test: check tick -5"
        );

        assert_eq!(
            clock.get_expected_tick_at(base_ts - tick_dur * 5 - tiny_dur),
            -6,
            "test: check tick -5 - tiny"
        );

        assert_eq!(
            clock.get_expected_tick_at(base_ts - tick_dur * 5 + tiny_dur),
            -5,
            "test: check tick -5 + tiny"
        );
    }

    #[test]
    fn test_partial() {
        let tick_dur_us = 50 * 1000;
        let tick_dur = time::Duration::from_micros(tick_dur_us);
        let tiny_dur = time::Duration::from_millis(1);
        let base_ts = time::Instant::now();

        let clock = Clock::new(base_ts, 0, tick_dur_us);

        // Have to do this annoying because of tuple return type.
        //
        // (millis relative to base ts, expected tick, expected proportion)
        let test_vectors: &[(i64, i64, f32)] = &[
            (0, 0, 0.0),
            (10, 0, 0.2),
            (50, 1, 0.0),
            (60, 1, 0.2),
            (100, 2, 0.0),
            (-10, -1, 0.8),
            (-50, -1, 0.0),
            (-40, -1, 0.2),
            (-100, -2, 0.0),
        ];

        for (inp, outpa, outpb) in test_vectors.iter().copied() {
            let ts = if inp > 0 {
                base_ts + time::Duration::from_millis(inp as u64)
            } else {
                base_ts - time::Duration::from_millis(-inp as u64)
            };

            let (a, b) = clock.get_partial_tick_at(ts);
            assert_eq!(a, outpa, "test: ({inp} ms) check tick {a}");
            assert!(
                test_approx_eq(b, outpb, 0.001),
                "test: ({inp:?} ms) check proportion {b}"
            );
        }
    }

    #[test]
    fn test_target_partial_simple() {
        let tick_dur_us = 50 * 1000;
        let base_ts = time::Instant::now();

        let clock = Clock::new(base_ts, 0, tick_dur_us);

        // Should get us ~0.2.
        let test_ts = base_ts + time::Duration::from_millis(10);
        let pt = clock.get_target_partial_tick(1, test_ts);
        eprintln!("pt {pt}");
        assert!(test_approx_eq(pt, 0.2, 0.001), "test: test partial tick");
    }

    /// Tests if we're way before the target tick.
    #[test]
    fn test_target_partial_before() {
        let tick_dur_us = 50 * 1000;
        let base_ts = time::Instant::now();

        let clock = Clock::new(base_ts, 0, tick_dur_us);

        // Should get us 0.0.
        let test_ts = base_ts + time::Duration::from_millis(10);
        let pt = clock.get_target_partial_tick(100, test_ts);
        eprintln!("pt {pt}");
        assert!(test_approx_eq(pt, 0.0, 0.001), "test: test partial tick");
    }

    #[test]
    fn test_target_partial_after() {
        let tick_dur_us = 50 * 1000;
        let base_ts = time::Instant::now();

        let clock = Clock::new(base_ts, 0, tick_dur_us);

        // Should get us 1.0.
        let test_ts = base_ts + time::Duration::from_millis(tick_dur_us * 2);
        let pt = clock.get_target_partial_tick(5, test_ts);
        eprintln!("pt {pt}");
        assert!(test_approx_eq(pt, 1.0, 0.001), "test: test partial tick");
    }
}
